<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h3 data-lake-id="8e1b944f" id="8e1b944f"><span data-lake-id="ub9a3ad52" id="ub9a3ad52">背景</span></h3>
  <p data-lake-id="ue801d606" id="ue801d606"><br></p>
  <p data-lake-id="ud23791d2" id="ud23791d2"><span data-lake-id="uf8520e28" id="uf8520e28">有这样一个业务场景，作为一个金融产品，很多用户会在借款后发生逾期，每一笔逾期都是一笔单独的借据，在贷后催收环节中，需要基于用户维度做聚合，把多笔借据合并成一个案件进行统一的催收。</span></p>
  <p data-lake-id="u0b77dac0" id="u0b77dac0"><br></p>
  <p data-lake-id="uee5a7c55" id="uee5a7c55"><span data-lake-id="u5216e2fa" id="u5216e2fa">那么就需要一个把多笔借据合并成一个案件的操作。最开始采用的方案就是定时任务扫表，每天早上凌晨5-8点之间进行定时任务扫表，然后进行案件的合并。</span></p>
  <p data-lake-id="ud65351dd" id="ud65351dd"><br></p>
  <p data-lake-id="uf2c5f53c" id="uf2c5f53c"><span data-lake-id="uec2c3aef" id="uec2c3aef">但是随着业务量的增多，扫表经常会扫不完，于是业务上为了提效，把定时任务改为分布式任务，借助多实例进行批量扫表。</span></p>
  <p data-lake-id="ucb7ab3bc" id="ucb7ab3bc"><br></p>
  <p data-lake-id="u706cfcfd" id="u706cfcfd"><span data-lake-id="ucaf23fe3" id="ucaf23fe3">但是这样做就导致数据库扛不住了，数据库的连接池经常在跑任务的时候被打满。于是就需要想办法解决这个问题。</span></p>
  <p data-lake-id="ucec40b3a" id="ucec40b3a"><br></p>
  <h3 data-lake-id="15a472cc" id="15a472cc"><span data-lake-id="u5029bbab" id="u5029bbab">技术选型</span></h3>
  <p data-lake-id="u9541977a" id="u9541977a"><br></p>
  <p data-lake-id="u29e246d7" id="u29e246d7"><span data-lake-id="ud9b74a23" id="ud9b74a23">关于这个问题，有挺多方案的。</span></p>
  <p data-lake-id="uac58e6b9" id="uac58e6b9"><br></p>
  <p data-lake-id="u6852a123" id="u6852a123"><span data-lake-id="u2674197e" id="u2674197e">首先就是可以选择分库分表，把原来的单独分成多个库，这样整体的连接数就多了，也就可以扛得住并发扫表了。但是这个方案比较重，分表后也会带来一系列问题。</span></p>
  <p data-lake-id="u5ea1c533" id="u5ea1c533"><br></p>
  <p data-lake-id="u0ee89762" id="u0ee89762"><span data-lake-id="u2110feff" id="u2110feff">于是考虑了另外一种方案，那就是基于"同步转异步"的思想，在借据生成的时候，就进行合并，而不是定时任务批量合并。</span></p>
  <p data-lake-id="u03b34790" id="u03b34790"><br></p>
  <p data-lake-id="u4c83e2c5" id="u4c83e2c5"><span data-lake-id="u66ad4f61" id="u66ad4f61">这样就可以把集中地流量分散到每一条借据生成的过程中，而且这个过程允许失败，一旦失败了，通过定时任务补偿即可。</span></p>
  <p data-lake-id="uf7c2ffa7" id="uf7c2ffa7"><br></p>
  <p data-lake-id="u2dd4c9b1" id="u2dd4c9b1"><span data-lake-id="ua2288997" id="ua2288997">但是这么做就会导致借据生成这部分逻辑很复杂，需要考虑到合并案件的事情，耦合性太深了。于是就基于Spring Event，把借据生成和案件合并进行解耦。</span></p>
  <p data-lake-id="uc183edbe" id="uc183edbe"><br></p>
  <p data-lake-id="uc9b8989c" id="uc9b8989c"><span data-lake-id="ue38d5d21" id="ue38d5d21">所以，整体方案就是基于Spring Event，实现同步转异步，解决定时任务扫表导致数据库连接池不够的问题。</span></p>
  <p data-lake-id="u901b590b" id="u901b590b"><br></p>
  <p data-lake-id="u519bcba2" id="u519bcba2"><span data-lake-id="u61c03b79" id="u61c03b79">在方案改造前，每次扫表需要处理的数据量有20万条，改造后，只需要1000左右的数据量需要扫表处理，大大提升系统的可用性。</span></p>
  <p data-lake-id="u8f5e19e1" id="u8f5e19e1"><br></p>
  <h3 data-lake-id="03f97796" id="03f97796"><span data-lake-id="u7a027d48" id="u7a027d48">你做了什么</span></h3>
  <p data-lake-id="udba319d1" id="udba319d1"><br></p>
  <p data-lake-id="ub19a0ac3" id="ub19a0ac3"><span data-lake-id="u15494e59" id="u15494e59">在借据生成的方法中，增加一个事件发送：</span></p>
  <p data-lake-id="u83969042" id="u83969042"><br></p>
  <pre lang="java"><code>

protected BaseManageResponse genenrateLoan(LoanGenerateEvent loanGenerateEvent) {
    BaseManageResponse manageResponse = new BaseManageResponse();

    try {
        //开启事务
        return transactionTemplate.execute(transactionStatus -&gt; {
            
            //核心逻辑执行
            doGenerateLoan(loanGenerateEvent);

            //发送一个案件入催完成的事件
            try {
                applicationContext.publishEvent(new CaseStartFinishEvent(loanGenerateEven));
            } catch (Exception e) {
                LOG.warn("publishLoanGenerateEventEvent failed", e);
            }

            //结果返回
            return manageResponse.successResponse(caseModel);
        });
    } catch (Exception e) {
        LoanGenerateStream existStream = queryExistStream(request);
        if (existStream != null) {
            return manageResponse.duplicatedResponse(existStream);
        }
        throw e;
    }

}
</code></pre>
  <p data-lake-id="uf3c98294" id="uf3c98294"><br></p>
  <p data-lake-id="u3464ece5" id="u3464ece5"><span data-lake-id="ubffc32c9" id="ubffc32c9">这里在</span><code data-lake-id="u519bb42d" id="u519bb42d"><span data-lake-id="uf1640399" id="uf1640399">applicationContext.publishEvent(new CaseStartFinishEvent(caseModel.getCaseItem()));</span></code><span data-lake-id="uceef070a" id="uceef070a">中发送一个事件，并且用try-catch包上，一旦失败了，不影响主流程。</span></p>
  <p data-lake-id="uf1049ed5" id="uf1049ed5"><br></p>
  <p data-lake-id="uac6c8ce6" id="uac6c8ce6"><span data-lake-id="ua3da1e7e" id="ua3da1e7e">然后再定义一个监听器，处理这个事件：</span></p>
  <p data-lake-id="ucb0bd688" id="ucb0bd688"><br></p>
  <pre lang="java"><code>

/**
 * 案件中心内部事件监听器
 *
 * @author Hollis
 */
@Component
public class CollectionCaseEventListener {

    @Autowired
    private CaseManageService caseManageService;

    @Autowired
    private DistributeLockSupport distributeLockSupport;
 
    @EventListener(CaseStartFinishEvent.class)
    @Async("caseStartFinishExecutor")
    public void onApplicationEvent(CaseStartFinishEvent event) {
        LoanGenerateEvent loanGenerateEvent = (LoanGenerateEvent) event.getSource();
        //加分布式锁，避免并发情况下导致创建多条案件
        if (!distributeLockSupport.acquireLock(loanGenerateEvent.getUserId(), loanGenerateEvent.getBizId(), 10000)) {
            return;
        }
        try {
            CaseMergeEvent mergeEvent = new CaseMergeEvent();
            mergeEvent.setCaseItemId(loanGenerateEvent.getId());
            mergeEvent.setUserId(loanGenerateEvent.getUserId());
            mergeEvent.setUserIdType(loanGenerateEvent.getUserIdType());
            mergeEvent.setIdentifier(UUID.randomUUID().toString());
            mergeEvent.setProduct(loanGenerateEvent.getProduct());
            mergeEvent.setBizId(loanGenerateEvent.getBizId());
            mergeEvent.setBizDate(new Date());
            caseManageService.merge(mergeEvent);
            
        } finally {
            distributeLockSupport.releaseLock(loanGenerateEvent.getUserId(), loanGenerateEvent.getBizId());
        }
    }
}
</code></pre>
  <p data-lake-id="uac8663cd" id="uac8663cd"><br></p>
  <p data-lake-id="u3d063386" id="u3d063386"><span data-lake-id="uac46aaac" id="uac46aaac">这里主要有一个点需要提一下，就是加了分布式锁，避免并发导致重复。即一锁二查三更新。</span></p>
  <p data-lake-id="u7e7ff503" id="u7e7ff503"><br></p>
  <h3 data-lake-id="e51e79c6" id="e51e79c6"><span data-lake-id="udc82f583" id="udc82f583">学习资料</span></h3>
  <p data-lake-id="u8ede3737" id="u8ede3737"><br></p>
  <p data-lake-id="u76887556" id="u76887556"><br></p>
  <p data-lake-id="ub5eedeec" id="ub5eedeec"><br></p>
  <p data-lake-id="u0de74772" id="u0de74772"><br></p>
 </body>
</html>